这是JS 原生方法原理探究系列的第三篇文章。本文会介绍如何模拟实现 new 操作符。关于 new 的具体用法,MDN 已经描述得很清楚了,这里我们只做简单的介绍,具体的重点在于如何模拟实现。

new 操作符的规范

下面展示的所有规范都是 ES5 版本的,与现在最新的规范有些区别

首先看一下根据规范的描述, new 操作符做了什么事:

全是英文,不过没关系,我简单翻译一下:

我在使用 new 操作符的时候,后面跟着的构造函数可能带参数,也可能不带参数,如果不带参数的话,比如说 new Fn(),那么这里这个 Fn 就是一个 NewExpression;如果带参数,比如说 new Fn(name,age),那么这里的 Fn 就是一个 MemberExpression

这两种情况下使用 new 操作符所进行的操作有点点不同,这里拿带参数的情况说明一下:

  1. 首先会对 Fn 这个 MemberExpression 求值,其结果是指向实际函数对象的一个引用,我们把这个引用作为 ref
  2. 接着调用 GetValue(ref) 进行求值,得到实际的函数对象,把这个对象作为 constructor
  3. Arguments 也就是传进来的参数求值,得到一个参数列表,作为 argList
  4. 如果 constructor 不是对象,则抛出类型错误
  5. 如果 constructor 没有实现内部的 [[Constructor]] 方法,也抛出类型错误
  6. 调用 constructor[[Constructor]]方法,并将 argList 传入作为参数,返回调用结果

从这些描述可以看出,更多的实现细节放在函数的 [[Constructor]] 方法里。那么这个方法具体是做什么用的呢?

[[Constructor]] 的规范

在 JS 中,函数有两种调用方式,一种是正常调用,这将调用函数的内部方法 [[Call]],还有一种是通过 new 调用,此时的函数作为一个构造函数,这将调用函数的另一个内部方法 [[Consturct]]。所以,要实现 new 操作的话,我们得先搞懂 [[Construct]] 内部方法做了什么事。

这里继续看规范是怎么说的:

简单翻译一下:

当通过可能为空的参数列表调用函数 F 的内部方法 [[Construct]] 的时候,会执行如下步骤:

  1. obj 作为一个新创建的原生对象
  2. 按照规范指定的,为 obj 设置所有内部方法
  3. obj 的内部属性 [[Class]] 设置为 Object
  4. 传参 prototype 调用函数 F 的内部方法 [[Get]],获取函数的原型对象,作为 proto
  5. 如果 proto 是对象,则将 obj 的内部属性 [[Prototype]] 设置为 proto
  6. 如果 proto 不是对象,则将 obj 的内部属性 [[Prototype]] 设置为标准内建的 Object 的原型对象
  7. 调用函数 F 的内部方法 Callobj 作为调用时的 this 值,此前传给 [[Construct]] 的参数列表作为调用时的参数。将调用后得到的结果作为 result
  8. 如果 result 是对象,则将其返回
  9. 否则,返回 obj

可以说,规范已经讲得很清楚了,简单地说,在 new 一个构造函数的时候,具体会做下面的事情:

  • 内部创建一个实例对象,并指定实例对象的原型:
    • 如果构造函数的原型是对象,则让实例的 __proto__ 等于构造函数的 prototype
    • 如果构造函数的原型不是对象,则让实例的 __proto__ 等于 Objectprototype
  • 将实例对象绑定为构造函数中的 this,此前传递进来的参数作为参数,并执行一遍构造函数
  • 如果构造函数返回了对象,则将其作为返回值,否则将实例对象作为返回值

代码实现

ES3 版本的实现如下:

function myNew(Fn){
    if(typeof Fn != 'function'){
        throw new TypeError(Fn + 'is not a constructor')
    }
    myNew.target = Fn
    var instance = {}
    // 检测构造函数原型是不是对象
    instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype 
    const returnValue = Fn.apply(instance,Array.prototype.slice.call(arguments,1))
    if(typeof returnValue === 'object' && returnValue !== null || typeof returnValue === 'function'){
        return returnValue
    } else {
        return instance
    }
}

ES6 版本的实现如下:

function myNew(Fn,...args){
    if(typeof Fn != 'function'){
        throw new TypeError(Fn + 'is not a constructor')
    }
    myNew.target = Fn
    const instance = {}
    // 检测构造函数原型是不是对象
    instance.__proto__ = Fn.prototype instanceof Object ? Fn.prototype : Object.prototype 
    const returnValue = Fn.call(instance,...args)
    return returnValue instanceof Object ? returnValue : instance
}

注意几个要点:

  • 当函数是通过 new 调用的时候,new.target 会指向函数自身,这个“指向”的操作在代码里就是通过 myNew.target = Fn 体现的
  • 为什么不直接使用 const instance = Object.create(Fn.prototype) 创建实例呢?根据规范,我们在实现 new 的时候,需要检测构造函数的原型是不是对象,如果不是对象,比如说是 null,那么实例的 __proto__ 会指向 Object 的原型,而这里如果使用了 Object.create,则会导致实例的 __proto__ 仍然指向 null。网上很多 new 的模拟实现直接使用了 Object.create,或者根本没有对构造函数的原型进行类型检查,这是不够严谨的
  • 如果无法使用 instanceof,我们也可以改用 typeof Fn.prototype === 'Object' && Fn.prototype !== null 进行判断